Fix uninitialized CUB reduction identity corrupting fill_holes vertices - #41
Open
rwfsmith wants to merge 1 commit into
Open
Fix uninitialized CUB reduction identity corrupting fill_holes vertices#41rwfsmith wants to merge 1 commit into
rwfsmith wants to merge 1 commit into
Conversation
`CuMesh::fill_holes` computes each new hole-cap vertex with
`cub::DeviceSegmentedReduce::Sum` over `Vec3f` (src/clean_up.cu:660).
CUB builds the reduction's initial value **on the host** as `InitT{}`.
Because `Vec3f` declares a *user-provided* default constructor, `Vec3f{}`
performs value-initialization by calling that constructor rather than
zero-initializing the members. The constructor was marked `__device__`
only, so it is not callable from host code and the initial value was
left as raw host stack memory.
CUB adds that identity into every segment, so each hole-cap vertex became
v_i = (garbage + sum(loop midpoints)) / n_i
i.e. every new vertex was offset by the same garbage vector scaled by
1/n_i. Whenever the stack slot happened to hold a large float, the newly
appended vertices were flung far outside the mesh, producing long sliver
triangles that render as heavy streaking. Components whose stack bytes
happened to be zero were unaffected, so the corruption typically showed
up in only one axis.
This is state-dependent rather than random: the first `fill_holes` call
in a process often lands on a zeroed stack slot and produces a correct
mesh, while every subsequent call reuses stack residue and reproduces the
same corruption. Observed on a TRELLIS.2 image-to-3D run, where the first
generation in a process was always clean and every later generation was
corrupted identically (26,556 bad vertices, max |z| ~4.8e9).
Marking the `Vec3f` constructors `__host__ __device__` makes the existing
zero-initializing constructor callable from host code, so CUB's identity
is a true zero. `atlas.cu` already uses the safe pattern for the analogous
`float3` reduction by passing `make_float3(0, 0, 0)` explicitly.
Verified on Windows 11 / CUDA 13.0 / RTX 5090 (sm_120) with 5 consecutive
image-to-3D generations in a single process: before the fix runs 1-7 were
all corrupted (max edge 4.76e9, 3910 degenerate long edges, silhouette
coverage 52.3% vs 30.0% expected); after the fix all runs are clean
(max edge ~0.005, 0 long edges, coverage 30.0%).
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
CuMesh::fill_holesproduces corrupted vertices whenever the CUB reduction identity happens to land on non-zero host stack memory. In practice the firstfill_holescall in a process is usually fine and every subsequent call is corrupted, which makes this look like non-determinism but is actually deterministic per-call-index state.This is a cross-platform correctness bug, not a Windows-specific one — the identity is constructed on the host, so it depends on host stack residue, not on the compiler or OS.
Root cause
fill_holescomputes each hole-cap vertex with a segmented reduction overVec3f:CUB materializes the reduction identity on the host as
InitT{}.Vec3fis declared with a user-provided default constructor:Two things combine:
Vec3f{}is value-initialization that calls the constructor rather than zero-filling.__device__only, so it is not callable from host code. CUB's host-sideInitT{}therefore yields whatever bytes were on the host stack.That garbage
Gis then folded into every segment's sum and divided by the segment size, so each bad vertex gets(G + Σ midpoints) / n.Evidence
Dumping the corrupted vertex buffer showed all bad Z values were exactly
G / nfor small integersn, with identical mantissas at different exponents:A single constant divided by the segment size can only come from a non-zero reduction identity. Only the Z component was affected here — the stack bytes backing
x/yhappened to be zero in this process, which is exactly the "sometimes it works" signature.The bad vertices were also the contiguous tail of the buffer (the newly appended hole-cap vertices), and face indices were all in range — consistent with
fill_holesappending bad vertices rather than any earlier stage emitting them.Fix
Make
Vec3f's constructors__host__ __device__so the host-side identity is actually constructed (and zeroed) instead of reading uninitialized stack memory. Six lines, no behavior change on the device side.atlas.cualready uses the safe pattern for its reduction (float3with an explicitmake_float3(0, 0, 0)identity); this bringsfill_holesin line with that.QEMin the same header has the same__device__-only user-provided constructor pattern, but it is not used in any CUB reduction today, so it is intentionally left unchanged to keep this fix surgical.Validation
Repro harness: N image-to-3D generations in a single process (TRELLIS.2), measuring max mesh edge length, long-edge count, and rendered mask coverage.
0/5 clean before, 5/5 clean after. Renders confirm it visually: the corrupted runs smeared the model into a cylindrical column of sliver triangles; after the fix every run reconstructs correctly.
Built and tested on Windows 11, MSVC 19.44, CUDA 13.0, PyTorch 2.13, RTX 5090 (sm_120).